Logo

Introduction

Stratton AE-Banking is a newly founded online bank in the US market. The E-banking service is a joint venture of a young fintech start-up and the long-time standing New York Stratton & Fils private banking house. The joint venture was founded in 2020 and has since then enjoyed great interest by providing digital private banking services. It profits from an AI driven recommender engine that uses past investment information together with a market and finance machine learning engine to derive investment tips and portfolio suggestions for its customers. So far, the fintech startup was well able to successfully approach young investors and customers. After the joint venture with Stratton & Fils, the fintech hopes to also attract existing customers from the established bank.

However, the conservative bank management of Stratton & Fils is extremely worried about simply approaching all of its customers, as it fears that the data driven and digital customer experience of Stratton AE may disturb some of its long-standing customers and may harm the longtime established and very intimate customer relations, which are believed to be an essential success factor in the bank’s success history.

The management thus approaches you as the head of the data science team and asks you to conduct a segmentation analysis of the bank’s existing customer base and to identify suitable customer segments, which might be open to try out Stratton & Fils joint venture. As a base for your segmentation analysis, the CRM manager provides you with the following data.

In [128]:
import pandas as pd
file_path = 'Markdown/Data/tabledesc.csv'

# Read the CSV file into a DataFrame
df = pd.read_csv(file_path)

# Display the DataFrame 
print(df)
                 Variable                                        Description  \
0                     Age                                       Customer Age   
1                  Income                               Household Net Income   
2           HouseholdSize               Number of People Living in Household   
3            CityAreaSize                       City or Main Area Population   
4          MeanCityIncome       Average Income on ZIP-Code and Street Level    
5      MeanCityHousePrize  Average House Prizes on ZIP-Code and Street Le...   
6   MeanCityHouseholdSize  Average Household Size on ZIP-Code and Street ...   
7       MeanCitySqFtPrice  Average Prizes per Square Foot on ZIP-Code and...   
8               NumbCars               Number of registered cars of customer   
9   InternetTrafficVolume  Volume of Internet Traffic per customer household   
10          MortageVolume                     Mortage to be paid by Customer   
11        AccountSpending         Monthly average spending from bank account   
12     CreditCardSpending          Monthly average spending from Credit Card   
13        HelpHotlineTime             Number of Minutes with Banking Hotline   
14          CustomerSince                    Time since opening bank account   
15        GrocerySpending  Average grocery related spendings from bank ac...   
16            StockVolume                                   Stock Investment   
17           CreditVolume                              Credits with the bank   
18           NASDAQInvest  Amount of money invested in NASDAQ listed comp...   
19        USAXSFundInvest  Amount of money invested in Stratton owned sha...   
20           BranchVisits  Number of recorded branch visits within the la...   
21              AppLogins  Number of customer logins in mobile banking ap...   
22             ATMVisitis  Number of times customer used an ATM service p...   
23      TimeOnlineBanking        Time logged into the Online Banking System    
24            ServiceFees               Extra Fees paid for banking services   
25       SocialMediaInter  Number of Finance Specific Social Media Profil...   
26               Bitcoins                Number of Bitcoins hold by customer   
27                    NFT                Number of NFTs bought by customer     

                   Measurement  
0                 Age in Years  
1            Net Income in USD  
2               Integer number  
3               Integer number  
4        Average Income in USD  
5        Average Prizes in USD  
6   Average Number Inhabitants  
7                       Yes/No  
8               Number of Cars  
9                           GB  
10                         USD  
11                         USD  
12                         USD  
13                     Minutes  
14                      Months  
15                         USD  
16                         USD  
17                         USD  
18                         USD  
19                         USD  
20              Integer number  
21              Integer number  
22              Integer number  
23                     Minutes  
24                         USD  
25              Integer number  
26                      Number  
27              Integer number  
In [129]:
file_path = 'Markdown/Data/StrattonAEBankingCRM.csv'

# Read the CSV file into a DataFrame
BankinCRMData = pd.read_csv(file_path)

# Display summary statistics of the DataFrame
print(BankinCRMData.describe())
                Age         Income  HouseholdSize   CityAreaSize  \
count  10750.000000   10750.000000   10750.000000   10750.000000   
mean      35.156651   84699.601953       2.809674  372196.129023   
std       13.542091   49337.110399       1.325119  228010.188666   
min       18.000000   35202.000000       1.000000   61613.000000   
25%       23.000000   42803.250000       2.000000  121703.750000   
50%       30.000000   71268.000000       3.000000  450100.000000   
75%       45.000000  125870.000000       4.000000  459418.000000   
max       74.000000  181863.000000       8.000000  708729.000000   

       MeanCityIncome  MeanCityHousePrize  MeanCityHouseHoldSize  \
count    10750.000000        1.075000e+04           10750.000000   
mean    163706.020279        9.425050e+05               3.022977   
std      64581.642188        7.136861e+05               1.338834   
min      35372.000000        1.250110e+05               1.000000   
25%     116252.750000        4.448165e+05               2.000000   
50%     140457.500000        6.146005e+05               3.000000   
75%     235000.000000        1.849915e+06               4.000000   
max     286996.000000        1.850000e+06               8.000000   

       MeanCitySqFtPrice    NumberCars  InternetTrafficVolume  ...  \
count       10750.000000  10750.000000           10750.000000  ...   
mean         5318.269023      1.383814              67.565023  ...   
std          2394.522297      0.925567              33.096996  ...   
min          1871.000000      0.000000               6.000000  ...   
25%          2627.000000      1.000000              45.000000  ...   
50%          5778.500000      1.000000              60.000000  ...   
75%          6741.000000      2.000000              86.000000  ...   
max          9886.000000      4.000000             118.000000  ...   

       NASDAQInvest  USAXSFundInvest  BranchVisits     AppLogins  \
count  10750.000000     10750.000000  10750.000000  10750.000000   
mean    1828.480162       761.645087      3.912558     55.704558   
std     1447.337174       836.464178      3.219362     34.641660   
min      228.352966        69.947903      0.000000      1.000000   
25%      401.400337       149.795783      2.000000     18.000000   
50%     1498.098994       313.824135      3.000000     64.000000   
75%     3056.049762      1060.232555      5.000000     82.000000   
max     4532.364575      3396.609135     20.000000    130.000000   

          ATMVisits  TimeOnlineBanking   ServiceFees  SocialMediaInter  \
count  10750.000000       10750.000000  10750.000000      10750.000000   
mean       4.928093         113.878607     40.938245         19.028837   
std        2.324207          59.183105     31.086657         17.050131   
min        0.000000          22.769131      0.134308          0.000000   
25%        3.000000          69.281909     17.844189          5.000000   
50%        5.000000          88.256085     27.238595         16.000000   
75%        7.000000         152.915443     50.165175         31.000000   
max       11.000000         232.214599    124.261270         60.000000   

           Bitcoins          NFTs  
count  10750.000000  10750.000000  
mean       0.193681      3.317395  
std        0.222972      2.691006  
min        0.000000      0.000000  
25%        0.000500      1.000000  
50%        0.099800      3.000000  
75%        0.400500      4.000000  
max        0.601400     12.000000  

[8 rows x 28 columns]

Distance as a measure of similarity¶

To identify segments of similar customers, let us first focus on the question how to measure similarity. Table 2 shows us some observations for customers from another banking database. The columns show the values of some customer related attributes. We can use the individual attribute characteristics to now calculate a so called distance measure, which shows how similar or dissimilar customers are. The higher the distance, the more dissimilar they are. For continuous variables, we can use the basic Euclidean Distance measure to derive similarities. The Euclidean Distance between two customers A and B can be expressed by the following equation.

$$ED_{A,B}= \sqrt{(f_{1,A}-f_{1,B})^{2}+(f_{2,A}-f_{2,B})^{2}+...+(f_{n,A}-f_{n,B})^{2}} $$
In [130]:
Table2_1 = pd.read_csv("Markdown/Data/mashcustomer.csv")
Table2_1.head()
Out[130]:
Customer Age Income Debt Household Size
0 Hawkeye 32 45,000 25,000 1
1 Potter 64 75,000 10,000 3
2 Burns 49 42,000 20,000 5
3 Hotlips 33 22,000 2,000 1
4 Klinger 29 16,000 6,000 4

We can now use the formula of the Euclidean Distance to calculate for example the distance between Hawkeye and Potter.

In [131]:
import math
In [132]:
ED_Hawkeye_Potter = math.sqrt((32-64)**2 + (45-75)**2 + (25-10)**2 + (1-3)**2)
print(ED_Hawkeye_Potter)
46.400431032480725

Question: Repeat the calculations for Hawkeye and Burns as well as Hawkeye and Hotlips.

In [133]:
ED_Hawkeye_Burns = math.sqrt((32-49)**2 + (45-22)**2+ (25-20)**2 + (1-5)**2) 
ED_Hawkeye_Burns
Out[133]:
29.30870177950569
In [134]:
ED_Hawkeye_Hotlips = math.sqrt((32-33)**2 + (45-42)**2+ (25-2)**2 + (1-1)**2) 
ED_Hawkeye_Hotlips
Out[134]:
23.2163735324878

While this is a great exercise, it will be impossible to calculate the distances amongst all members of a large customer data base with e.g. 200,000 entries. However in this case we can also use R's function for Euclidean Distances. We simply need to give the function a data frame with all observations we would like to compare, and R will return a table with the corresponding distances.

In [135]:
from scipy.spatial.distance import euclidean
from scipy.spatial.distance import pdist, squareform

# Convert 'Income' and 'Debt' columns to numeric
Table2_1['Income'] = pd.to_numeric(Table2_1['Income'].str.replace(',', ''))
Table2_1['Debt'] = pd.to_numeric(Table2_1['Debt'].str.replace(',', ''))

# Extracting columns 2 to 5 (Age, Income, Debt, Household Size) for distance calculation
subset_data = Table2_1.iloc[:, 1:5]

# Calculate pairwise Euclidean distances
distances = pdist(subset_data, metric='euclidean')

# Convert to squareform for better readability
dist_matrix = squareform(distances)

# Create a DataFrame with the distances
dist_df = pd.DataFrame(dist_matrix, index=Table2_1['Customer'], columns=Table2_1['Customer'])

# Display the result
print(f"Metric: 'euclidean'; comparing: {len(subset_data)} vectors.")
print(dist_df)
Metric: 'euclidean'; comparing: 5 vectors.
Customer       Hawkeye        Potter         Burns       Hotlips       Klinger
Customer                                                                      
Hawkeye       0.000000  33541.034987   5830.978048  32526.911950  34669.871906
Potter    33541.034987      0.000000  34481.882620  53600.382135  59135.448134
Burns      5830.978048  34481.882620      0.000000  26907.253149  29529.652910
Hotlips   32526.911950  53600.382135  26907.253149      0.000000   7211.104284
Klinger   34669.871906  59135.448134  29529.652910   7211.104284      0.000000

k-mean as a solution to form homogenous subgroups¶

While the distances help us with understanding similarities and dissimilarities they do not yet help us with forming subgroups, ad only from the distances, you do not know which threshold determines similarity/dissimilarity. Hawkeye may be closest to Burns, but is 18 still a great distance? Or actually already pretty similar? Who should be paired with whom?

This implies that grouping consumers in homogenous subgroups requires a lot of attention and balance and some more information than just similarity measures. In addition, we realized with our 5 customers, that grouping takes us some time and effort and will certainly prevent us from forming larger groups or segmenting larger data sets with hundreds of thousands of customers. Therefore, it is time to discover a method that uses intersubject distances to automatically form groups. Such methods are commonly referred to as cluster analysis. Cluster analysis are a well-known and established statistical method, that is used for the last 30-40 years in marketing research. With the advent of machine learning and artificial intelligence applications, cluster analysis became again popular in data science, where it is often referred to as an unsupervised learning algorithm.

k-mean cluster analysis uses distances to form clusters within data. Once the user determined the k number of clusters the algorithm should define, the cluster randomly assigns k starting points within the data (Step1). It continues then to calculate the distance of each observation in the data to each starting point. As pointed out in in below's Figure the algorithm then assigns each observation according to the distance to the closest starting point (Step2). This leads to an initial cluster solution. For each of these clusters the algorithm then calculates the new center point of the cluster, called centroid (Step3). The centroid can be interpreted as the mean of all observations within this cluster. Step4 now repeats the procedure of step2. The new centroids are used to again calculate all distances between all observations and all centroids. Then again, the observations are assigned to the closest centroid. This may lead to changes in cluster membership and lead to new forms of clusters. In the subsequent step, the algorithm continues to calculate the resulting new centroids (Step5), to then re-calculate the distances and re-assigning observations to clusters. The algorithm stops once no observation can be re-assigned to another cluster or after a to be specified number of iterations.

Logo

One thing we may mind before running a cluster analysis, is scale heterogeneity. Especially k-mean clustering is sensitive to data that comes at different scale levels. Having variables at very different levels, thus creates problems, which may ultimately lead to biased results. A quick fix is to standardize the variables so that they all share a similar range. This procedure is commonly referred to as standardization.

R can standardize all variables for us with the help of the scale() function. When we now inspect the resulting new data frame scaled.crm with the head() function.

In [136]:
from sklearn.preprocessing import StandardScaler
In [137]:
# Extracting numeric columns for scaling
numeric_columns = BankinCRMData.select_dtypes(include=['int64', 'float64']).columns
numeric_data = BankinCRMData[numeric_columns]

# Scale the numeric data
scaler = StandardScaler()
scaled_crm = scaler.fit_transform(numeric_data)

# Create a DataFrame with the scaled data
scaled_crm_df = pd.DataFrame(scaled_crm, columns=numeric_columns)

# Display the head of the scaled DataFrame
print(scaled_crm_df.head(6))

###CHECK : The values are not an exact match with that in the R-Code. They are rounded off.
        Age    Income  HouseholdSize  CityAreaSize  MeanCityIncome  \
0  0.357668 -0.102901      -0.611049      0.361798       -1.130994   
1  0.136126 -0.265200       1.653005      0.352057       -0.107838   
2  2.646934 -0.125177      -1.365733      0.370167       -1.722273   
3  1.317683 -0.299213       0.143636      0.370167       -0.701223   
4  0.357668 -0.211932      -1.365733      0.350035       -1.974012   
5 -0.233110 -0.181649       3.162374      0.368189       -1.121269   

   MeanCityHousePrize  MeanCityHouseHoldSize  MeanCitySqFtPrice  NumberCars  \
0            1.271589              -0.017163          -0.628659    0.665770   
1            1.271058               1.476744          -0.022665    0.665770   
2            1.271554              -0.017163          -0.799056   -0.414699   
3            1.270641               0.729791          -1.326952   -1.495169   
4            1.270564               2.223698          -1.319017   -0.414699   
5            1.271276              -0.017163          -0.264060   -0.414699   

   InternetTrafficVolume  ...  NASDAQInvest  USAXSFundInvest  BranchVisits  \
0              -0.289013  ...     -0.235347        -0.341807     -0.283472   
1              -0.953757  ...     -0.224107        -0.324460      0.027162   
2              -0.319229  ...     -0.226961        -0.354929     -0.283472   
3               0.103790  ...     -0.229419        -0.327110      0.027162   
4              -0.863110  ...     -0.226898        -0.372217     -0.283472   
5              -0.470307  ...     -0.225932        -0.231325      0.027162   

   AppLogins  ATMVisits  TimeOnlineBanking  ServiceFees  SocialMediaInter  \
0  -1.319414   1.752037          -0.715268     0.028782          0.467535   
1  -1.059599   1.321763          -0.789812     0.361236          0.350228   
2  -1.203941   1.321763          -0.928995     0.440483          0.526188   
3  -1.203941   1.321763          -0.894188     0.027947          0.878108   
4  -1.146204   1.752037          -0.789528     0.520945          0.878108   
5  -1.232809   1.321763          -0.976702     0.518330          0.291575   

   Bitcoins      NFTs  
0 -0.854321 -0.489578  
1 -0.852078 -0.861203  
2 -0.807676 -0.861203  
3 -0.861497 -0.861203  
4 -0.835035 -1.232829  
5 -0.855218 -0.117952  

[6 rows x 28 columns]

As you see, all variables now range in similar areas. We can thus proceed with our analysis

We can now start with the cluster analysis. Let us first try out different solutions with different numbers of clusters. To ensure that we start with the same centroids, we use the set.seed function. This ensures that every time we run this code, we end up with the same results. If you do not use set.seed ahead of the cluster analysis, you will receive different solutions, which will be close to each other but not identical. We can run a k-mean cluster analysis with NumPy's kmean function (the library is imported at the begining) We tell the kmean function simply which data.frame contains our customer data and specify the k number of clusters we want to be included. Here we set k to 4.

To see how many customers are assigned to each cluster, we furthermore plot the cluster sizes with the help of ggplot and a simple barplot.

In [138]:
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
import seaborn as sns

np.random.seed(13)
# Perform k-means clustering
kmeans = KMeans(n_clusters=4, random_state=13, init='k-means++', n_init=10)
StrattonCluster_4k = kmeans.fit_predict(scaled_crm_df)

# Get cluster sizes
sizes4k = pd.DataFrame({'Size': np.bincount(StrattonCluster_4k), 'Cluster': [f'Cluster{i+1}' for i in range(4)]})

# Plot the cluster sizes 
plt.figure(figsize=(10, 6))
sns.barplot(x='Cluster', y='Size', data=sizes4k, color='turquoise')
plt.xlabel('Cluster')
plt.ylabel('Size')
plt.title('Cluster sizes k-means 4-cluster solution')
for i, size in enumerate(sizes4k['Size']):
    plt.text(i, size + 0.1, str(size), ha='center', va='bottom')
plt.show()

print(sizes4k)
   Size   Cluster
0  4500  Cluster1
1  2250  Cluster2
2  1500  Cluster3
3  2500  Cluster4

We can now inspect the different clusters and check their mean values. We achieve this with the following code, that first matches the estimated cluster to each observation in our data frame. Subsequently, we use dyplr’s group_by command to calculate the mean of each variable per cluster. You can then inspect the resulting data frame. You will see that some of the clusters show substantially different mean values for specific variables, while in other cases the means do not vary across the clusters.

In [139]:
# Add the cluster labels to BankinCRMData
BankinCRMData['k4Cluster'] = StrattonCluster_4k

# Group by k4Cluster and calculate mean for numeric columns
summarystats_percluster_4k = BankinCRMData.groupby('k4Cluster').agg('mean')

# Display the head of the summary statistics
print(summarystats_percluster_4k.head())
                 Age         Income  HouseholdSize   CityAreaSize  \
k4Cluster                                                           
0          32.132667   85293.995333       2.605556  239607.172667   
1          39.363111   71856.288444       2.703556  452187.049778   
2          57.965333  180029.459333       3.488000  120068.719333   
3          23.128800   37990.761600       2.865600  690140.867600   

           MeanCityIncome  MeanCityHousePrize  MeanCityHouseHoldSize  \
k4Cluster                                                              
0           115856.230667        8.628481e+05               2.985333   
1           179344.236444        3.085286e+05               2.963111   
2           139990.476667        6.200123e+05               3.366667   
3           249990.573200        1.849962e+06               2.938400   

           MeanCitySqFtPrice  NumberCars  InternetTrafficVolume  ...  \
k4Cluster                                                        ...   
0                3528.396667    1.480889              51.697556  ...   
1                4458.828444    0.803111             115.006222  ...   
2                6497.381333    2.770000              15.015333  ...   
3                8606.068400    0.900000              84.959200  ...   

           NASDAQInvest  USAXSFundInvest  BranchVisits   AppLogins  ATMVisits  \
k4Cluster                                                                       
0           1117.043945       767.378607      4.030222   41.641778   4.518000   
1           4055.420171      1870.605450      1.404000  103.894222   4.928889   
2           3003.141149       100.033983      9.985333   10.025333   4.036000   
3            400.022754       150.227085      2.314800   65.054400   6.200800   

           TimeOnlineBanking  ServiceFees  SocialMediaInter  Bitcoins  \
k4Cluster                                                               
0                 113.403867    21.637420         12.269333  0.134875   
1                 202.818078    94.546763         46.492889  0.544434   
2                  30.036049    44.874469          2.502667  0.000102   
3                  84.993150    25.070329         16.394000  0.099999   

               NFTs  
k4Cluster            
0          1.969333  
1          7.879111  
2          1.000000  
3          3.028800  

[4 rows x 28 columns]

Another approach to assess the quality of our segmentation, is to plot the different clusters. A key challenge here is dimensionality. Given that our clusters depend on a multitude of variables, we cannot plot them all together. To come to a solution that we can plot, we need to reduce the dimensions to two main factors, which then allows us to plot the points in two-dimensional space. A common technique to achieve this is a principal component analysis (PCA) that reduces all variables to two main factors, which we can subsequently plot. The plot will then allow us to better see if clusters overlap or if we end up with a meaningful separation between the different identified clusters. R’s factoextra package offers various functions, which achieve this with a single command that does not require us to code the PCA nor the plot.

In [140]:
import numpy as np
from sklearn.decomposition import PCA
from sklearn.mixture import GaussianMixture
from matplotlib.patches import Ellipse

# Define the plot_ellipse function
def plot_ellipse(ax, mean, covariances, color, alpha):
    v, w = np.linalg.eigh(covariances)
    v = 2.0 * np.sqrt(2.0) * np.sqrt(v)
    u = w[0] / np.linalg.norm(w[0])
    angle = np.arctan(u[1] / u[0])
    angle = 180.0 * angle / np.pi
    ell = Ellipse(mean, width=v[0], height=v[1], angle=angle, color=color, alpha=alpha)
    ax.add_patch(ell)

# Use PCA for dimensionality reduction (optional)
pca = PCA(n_components=2)
reduced_data = pca.fit_transform(scaled_crm_df)

# Create a DataFrame with cluster labels and reduced data (optional)
cluster_data = pd.DataFrame({'Cluster': StrattonCluster_4k, 'PC1': reduced_data[:, 0], 'PC2': reduced_data[:, 1]})

# Plot the clusters using seaborn
plt.figure(figsize=(10, 6))
sns.scatterplot(x='PC1', y='PC2', hue='Cluster', data=cluster_data, palette='viridis', s=100, alpha=0.7)
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Cluster Visualization k-means 4-cluster solution')

# Plot ellipsoids around each cluster
for i in range(4):
    cluster_points = reduced_data[StrattonCluster_4k == i]
    
    # Fit a Gaussian Mixture Model to estimate the covariance matrix
    gmm = GaussianMixture(n_components=1, covariance_type='full')
    gmm.fit(cluster_points)
    
    # Get the covariance matrix and mean of the cluster
    covariances = gmm.covariances_
    mean = gmm.means_
    
    # Plot the ellipsoid
    plot_ellipse(ax=plt.gca(), mean=mean[0], covariances=covariances[0], color='black', alpha=0.3)

# Adjusting the legend
handles, labels = plt.gca().get_legend_handles_labels()
labels = [str(int(label) + 1) for label in labels]  # Incrementing cluster numbers in the legend
plt.legend(handles, labels, title='Cluster')

plt.show()

A quick inspection of the plot already reveals that our 4k cluster approach is not optimal, as we see some more separable groups of close to each other observations. Especially in case of Clusters 0 and 1 (the larger ones) it looks like we can still split these groups into two more subgroups each.

Question: Repeat the cluster analysis with k = 5 and k = 6

In [141]:
# k = 5
kmeans = KMeans(n_clusters=5, random_state=13, init='k-means++', n_init=10)
StrattonCluster_5k = kmeans.fit_predict(scaled_crm_df)

# Get cluster sizes
sizes5k = pd.DataFrame({'Size': np.bincount(StrattonCluster_5k), 'Cluster': [f'Cluster{i+1}' for i in range(5)]})

# Plot the cluster sizes 
plt.figure(figsize=(10, 6))
sns.barplot(x='Cluster', y='Size', data=sizes5k, color='turquoise')
plt.xlabel('Cluster')
plt.ylabel('Size')
plt.title('Cluster sizes k-means 5-cluster solution')
for i, size in enumerate(sizes5k['Size']):
    plt.text(i, size + 0.1, str(size), ha='center', va='bottom')
plt.show()

print(sizes5k)
   Size   Cluster
0  3000  Cluster1
1  2500  Cluster2
2  2250  Cluster3
3  1500  Cluster4
4  1500  Cluster5
In [142]:
# k = 6

kmeans = KMeans(n_clusters=6, random_state=13, init='k-means++', n_init=10)
StrattonCluster_6k = kmeans.fit_predict(scaled_crm_df)

# Get cluster sizes
sizes6k = pd.DataFrame({'Size': np.bincount(StrattonCluster_6k), 'Cluster': [f'Cluster{i+1}' for i in range(6)]})

# Plot the cluster sizes 
plt.figure(figsize=(10, 6))
sns.barplot(x='Cluster', y='Size', data=sizes6k, color='turquoise')
plt.xlabel('Cluster')
plt.ylabel('Size')
plt.title('Cluster sizes k-means 6-cluster solution')
for i, size in enumerate(sizes6k['Size']):
    plt.text(i, size + 0.1, str(size), ha='center', va='bottom')
plt.show()

print(sizes6k)
   Size   Cluster
0  3000  Cluster1
1  2500  Cluster2
2  1250  Cluster3
3  1500  Cluster4
4  1500  Cluster5
5  1000  Cluster6
In [143]:
# k = 7

kmeans = KMeans(n_clusters=7, random_state=13, init='k-means++', n_init=10)
StrattonCluster_7k = kmeans.fit_predict(scaled_crm_df)

# Get cluster sizes
sizes7k = pd.DataFrame({'Size': np.bincount(StrattonCluster_7k), 'Cluster': [f'Cluster{i+1}' for i in range(7)]})
In [144]:
# k = 8

kmeans = KMeans(n_clusters=8, random_state=13, init='k-means++', n_init=10)
StrattonCluster_8k = kmeans.fit_predict(scaled_crm_df)

# Get cluster sizes
sizes8k = pd.DataFrame({'Size': np.bincount(StrattonCluster_8k), 'Cluster': [f'Cluster{i+1}' for i in range(8)]})

How to determine the right number of clusters

Trying out different solutions may point you onto something, still you will realize that determining the right number of clusters can be tricky.

To find the "best" number of cluster, there are different approaches and measures available. Before we discuss these, let us first reflect again on what we want to achieve with a cluster analysis.

We want to obtain subgroups that are homogenous within. So, to say we try to maximize within-group homogeneity, which means we try to reduce the level of variance between members of a cluster. The overall level of within-cluster-variances across all identified clusters can thus be used to describe the total degree of homogeneity obtained with a specific cluster solution. This gives us a chance to compare different cluster analyses with different numbers of clusters, as we can try to minimize the overall variances.

Using the within-cluster-variance values, we can determine which solution works best and then focus on this cluster analysis. To do so, we first estimate n cluster solutions with cluster numbers from 1 to k. Subsequently, we can then plot the within-cluster variance sums for each cluster solution.

Again Python can do this for us with some short lines of code. Below you find two measures for within-cluster variances. We can now ask Python to estimate kmean models with k values from 2 to 15 and to then plot the within variances of each solution. Don't worry, if this takes some time.

In [145]:
# If you do not have the yellowbrick library installed uncomment the line below and run it
# pip install yellowbrick
In [146]:
from sklearn.impute import SimpleImputer
from yellowbrick.cluster import KElbowVisualizer
from sklearn.metrics import silhouette_score

imputer = SimpleImputer(strategy="mean")
scaled_crm_imputed = imputer.fit_transform(scaled_crm_df)

# Elbow Method with explicit n_init
model = KMeans(n_init=10)  # You can set the value based on your preference
visualizer = KElbowVisualizer(model, k=(2, 15), metric='distortion', timings=False)
visualizer.fit(scaled_crm_imputed)
visualizer.show()

# Average Silhouette Width for k=2 to k=15 without visualization
avg_silhouette_scores = []
for k in range(2, 16):
    model = KMeans(n_clusters=k, random_state=13, n_init=10)
    labels = model.fit_predict(scaled_crm_imputed)
    avg_silhouette_scores.append(silhouette_score(scaled_crm_imputed, labels))

# Plotting Average Silhouette Width
plt.figure(figsize=(10, 6))
plt.plot(range(2, 16), avg_silhouette_scores, marker='o')
plt.xlabel('Number of Clusters (k)')
plt.ylabel('Average Silhouette Width')
plt.title('Average Silhouette Width for Different Values of k')
plt.show()

The Elbow plot (1st plot), shows the total within sum of cluster variances for all estimated 15 solutions. The rule of thumb states, that the optimal cluster number lays within the “elbow” of the plot. This seems to be here rather tricky. As the function drops immediately and shows very low summed variances for clusters 2 to 15. Therefore, we rely on a second method, the Silhouette plot. The silhouette coefficient measures of how close an object is to its own cluster centroid, compared to the one of other clusters. The coefficient ranges from −1 to +1. High values indicate strong separation. Low values indicate poor separation. We thus want to select the cluster solution with the highest silhouette coefficient. In our case, the plot suggests 8 clusters. Looking again at the Elbow plot on the left, 8 seems rather high, especially as the “Elbow” – lays somewhere between 5 and 7. The silhouette plot suggests that the 7-cluster solution is inferior to the 6- and 8-cluster solutions. We may thus enrich our insights by plotting all three solutions with the following command.

In [147]:
# Create a DataFrame with cluster labels and reduced data (optional)
cluster_data = pd.DataFrame({'Cluster': StrattonCluster_5k, 'PC1': reduced_data[:, 0], 'PC2': reduced_data[:, 1]})

# Plot the clusters using seaborn
plt.figure(figsize=(10, 6))
sns.scatterplot(x='PC1', y='PC2', hue='Cluster', data=cluster_data, palette='viridis', s=100, alpha=0.7)
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Cluster Visualization k-means 5-cluster solution')

handles, labels = plt.gca().get_legend_handles_labels()
labels = [str(int(label) + 1) for label in labels]  # Incrementing cluster numbers in the legend
plt.legend(handles, labels, title='Cluster')


plt.show()
In [148]:
# Create a DataFrame with cluster labels and reduced data (optional)
cluster_data = pd.DataFrame({'Cluster': StrattonCluster_6k, 'PC1': reduced_data[:, 0], 'PC2': reduced_data[:, 1]})

# Plot the clusters using seaborn
plt.figure(figsize=(10, 6))
sns.scatterplot(x='PC1', y='PC2', hue='Cluster', data=cluster_data, palette='viridis', s=100, alpha=0.7)
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Cluster Visualization k-means 6-cluster solution')

handles, labels = plt.gca().get_legend_handles_labels()
labels = [str(int(label) + 1) for label in labels]  # Incrementing cluster numbers in the legend
plt.legend(handles, labels, title='Cluster')

plt.show()
In [149]:
# Create a DataFrame with cluster labels and reduced data (optional)
cluster_data = pd.DataFrame({'Cluster': StrattonCluster_7k, 'PC1': reduced_data[:, 0], 'PC2': reduced_data[:, 1]})

# Plot the clusters using seaborn
plt.figure(figsize=(10, 6))
sns.scatterplot(x='PC1', y='PC2', hue='Cluster', data=cluster_data, palette='viridis', s=100, alpha=0.7)
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Cluster Visualization k-means 7-cluster solution')

handles, labels = plt.gca().get_legend_handles_labels()
labels = [str(int(label) + 1) for label in labels]  # Incrementing cluster numbers in the legend
plt.legend(handles, labels, title='Cluster')

plt.show()
In [150]:
# Create a DataFrame with cluster labels and reduced data (optional)
cluster_data = pd.DataFrame({'Cluster': StrattonCluster_8k, 'PC1': reduced_data[:, 0], 'PC2': reduced_data[:, 1]})

# Plot the clusters using seaborn
plt.figure(figsize=(10, 6))
sns.scatterplot(x='PC1', y='PC2', hue='Cluster', data=cluster_data, palette='viridis', s=100, alpha=0.7)
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('Cluster Visualization k-means 8-cluster solution')

handles, labels = plt.gca().get_legend_handles_labels()
labels = [str(int(label) + 1) for label in labels]  # Incrementing cluster numbers in the legend
plt.legend(handles, labels, title='Cluster')

plt.show()

Interpretation of Output

Let us first start by looking in more detail at our 7 -cluster k-mean model and see how big each cluster is, with the following code.

In [151]:
# 8 cluster k-mean cluster size plot

# Plot the cluster sizes 
plt.figure(figsize=(10, 6))
sns.barplot(x='Cluster', y='Size', data=sizes7k, color='turquoise')
plt.xlabel('Cluster')
plt.ylabel('Size')
plt.title('Cluster sizes k-means 7-cluster solution')
for i, size in enumerate(sizes7k['Size']):
    plt.text(i, size + 0.1, str(size), ha='center', va='bottom')
plt.show()

print(sizes7k)
   Size   Cluster
0  1500  Cluster1
1  2500  Cluster2
2  1250  Cluster3
3  1500  Cluster4
4  1500  Cluster5
5  1500  Cluster6
6  1000  Cluster7

To gain deeper insights related to spending behavior as well as the digital affinity of the different segments, we want to plot the means of the different variables. To achieve this we first again assemble a descriptive data set with all variable means per cluster with the help of dplyr’s group_by function.

In [152]:
# Build Mean per Cluster DataFrame
BankinCRMData['k7Cluster'] = StrattonCluster_7k

# Group by k4Cluster and calculate mean for numeric columns
summarystats_percluster_7k = BankinCRMData.groupby('k7Cluster').agg('mean')

We can now generate barplots of the different variables of interest and see if we find promising segments of Stratton & Fils customers, who might be open and suitable for Stratton AE Banking. Let us first focus on spending behavior, as indicated by the service fee variable. Note, that we adapted some of the commands in ggplot. By leaving geom_col() blank we do not specify a color and the plot remains in grey. In addition, we ask ggplot in geom_text to add labels with the two-digit rounded values of ServiceFees in white color and in font size 2. With the position_stack command we put the values in the middle of the barplot.

In [153]:
import matplotlib.pyplot as plt

# Barplot of Service Fees
plt.bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['ServiceFees'], color='grey')
plt.xlabel("Clusters")
plt.ylabel("Spending")
plt.title("Average Spending in Service Fees per Cluster")

# Adding text labels with values on top of each bar
for i, value in enumerate(summarystats_percluster_7k['ServiceFees']):
    plt.text(i+1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

plt.show()

A visual inspection indicates that clusters 3 and 7 show the highest spending behavior, with clusters 5 and 6 following, while the remaining clusters show rather low service fee spendings. This makes at least the four high spending segments attractive for AE Banking. However, to be sure that the rather novel and highly digital app service appeals to these segments, we need to understand how digitally active and interested these segments are.

Let us first focus on the latest developments in fintech such as Bitcoin and NFT investments. We can again compare the segment-specific means for both variables. This time we want to combine the plots of Bitcoins and NFTs in one plot. We can arrange this with ggplot’s facet_wrap function that allows us to combine plots of different variables. The only “complication” we need to address is that we need to re-arrange the data set we want to plot. We can again use dplyr for this. We first select the variables of interest (cluster, NFTs and Bitcoins) and then transpose the data frame from a wide to a narrow format. We can then use again ggplot. This time we use the geom_bar command instead of the geom_col command. Facet_wrap will now tell ggplot to make two plots and combine them under each other (col =1). By setting scales to “free_y” we allow different y-axis levels, given that scales substantially vary across the two different variables.

In [154]:
# Barplot of BitCoins
plt.bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['Bitcoins'], color='grey')
plt.xlabel("Clusters")
plt.ylabel("Value")
plt.title("Average Bitcoins per Cluster")

# Adding text labels with values on top of each bar
for i, value in enumerate(summarystats_percluster_7k['Bitcoins']):
    plt.text(i+1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

plt.show()

# Barplot of NFTs
plt.bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['NFTs'], color='grey')
plt.xlabel("Clusters")
plt.ylabel("Value")
plt.title("Average NFTs per Cluster")

# Adding text labels with values on top of each bar
for i, value in enumerate(summarystats_percluster_7k['NFTs']):
    plt.text(i+1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

plt.show()

From the inspection, we can see that clusters 3 and 7 show both most activity in NFTs acquisitions and are also most invested in Bitcoins, which makes them even more suitable for AE Banking. Let us now look at digital activities and compare digital and offline activities.

With the following code we can inspect the means for BranchVisits, AppLogins, ATMVisits, TimeOnlineBanking, SocialMediaInter, InternetTrafficVolume. As you can see from facet_wrap we now include two columns.

In [155]:
import matplotlib.pyplot as plt

# Create a 2x3 grid of subplots
fig, axs = plt.subplots(3, 2, figsize=(20, 15))

# Barplot of AppLogins
axs[0, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['AppLogins'], color='grey')
axs[0, 0].set(xlabel="Clusters", ylabel="Value", title="Average AppLogins per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['AppLogins']):
    axs[0, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of BranchVisits
axs[0, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['BranchVisits'], color='grey')
axs[0, 1].set(xlabel="Clusters", ylabel="Value", title="Average BranchVisits per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['BranchVisits']):
    axs[0, 1].text(i, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of SocialMediaInter
axs[1, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['SocialMediaInter'], color='grey')
axs[1, 0].set(xlabel="Clusters", ylabel="Value", title="Average SocialMediaInter per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['SocialMediaInter']):
    axs[1, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of ATMVisits
axs[1, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['ATMVisits'], color='grey')
axs[1, 1].set(xlabel="Clusters", ylabel="Value", title="Average ATMVisits per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['ATMVisits']):
    axs[1, 1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of InternetTrafficVolume
axs[2, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['InternetTrafficVolume'], color='grey')
axs[2, 0].set(xlabel="Clusters", ylabel="Value", title="Average InternetTrafficVolume per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['InternetTrafficVolume']):
    axs[2, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of TimeOnlineBanking
axs[2, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['TimeOnlineBanking'], color='grey')
axs[2, 1].set(xlabel="Clusters", ylabel="Value", title="Average TimeOnlineBanking per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['TimeOnlineBanking']):
    axs[2, 1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Adjust layout to prevent clipping of titles
plt.tight_layout()

# Show the plots
plt.show()

The plot further confirms the strong digital affinity of clusters 3 and 7. Both show the lowest number of branch visits, while showing strong activity in online baking, internet traffic, social media interest, and banking app logins. While we can now be sure that customers from segments 3 and 7 are highly digital affine and are thus likely to be interested in AE Banking, we should in the next step control the financial situation of these customers. Let us first focus on average age, income and household sizes.

In [156]:
# Create a 3x1 grid of subplots
fig, axs = plt.subplots(3, 1, figsize=(5, 7.5))

# Barplot of Age
axs[0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['Age'], color='grey')
axs[0].set(xlabel="Clusters", ylabel="Value", title="Average Age per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['Age']):
    axs[0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of HouseholdSize
axs[1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['HouseholdSize'], color='grey')
axs[1].set(xlabel="Clusters", ylabel="Value", title="Average HouseholdSize per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['HouseholdSize']):
    axs[1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of Income
axs[2].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['Income'], color='grey')
axs[2].set(xlabel="Clusters", ylabel="Value", title="Average Income per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['Income']):
    axs[2].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Adjust layout to prevent clipping of titles
plt.tight_layout()

# Show the plots
plt.show()

The plots reveal the problems with socio-economic clustering, as the results for age and household size do not vary too much across the 7 clusters. We see some variation for income, where clusters 3 and 7 remain close to the total mean of the dataset, indicating, that the digital affine users, we identified, are neither poor nor rich, making them still a suitable target group. Age-wise, we similarly see that both segments are well-established adults in their end 30s or early 40s. Given that the socio-economic information indicates that the digital affine users profit from stable incomes, we should in the next steps focus on spending and investment behavior to understand, whether these segments allow sufficient business volume and growth potential.

In [157]:
#Investment Cluster Means

# Create a 2x3 grid of subplots
fig, axs = plt.subplots(2, 2, figsize=(17, 12))

# Barplot of MortageVolume
axs[0, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['MortageVolume'], color='grey')
axs[0, 0].set(xlabel="Clusters", ylabel="Value", title="Average MortageVolume per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['MortageVolume']):
    axs[0, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of NASDAQInvest
axs[0, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['NASDAQInvest'], color='grey')
axs[0, 1].set(xlabel="Clusters", ylabel="Value", title="Average NASDAQInvest per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['NASDAQInvest']):
    axs[0, 1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of StockVolume
axs[1, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['StockVolume'], color='grey')
axs[1, 0].set(xlabel="Clusters", ylabel="Value", title="Average StockVolume per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['StockVolume']):
    axs[1, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of USAXSFundInvest
axs[1, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['USAXSFundInvest'], color='grey')
axs[1, 1].set(xlabel="Clusters", ylabel="Value", title="Average USAXSFundInvest per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['USAXSFundInvest']):
    axs[1, 1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Adjust layout to prevent clipping of titles
plt.tight_layout()

# Show the plots
plt.show()
In [158]:
#Plots for Spending and Investments

# Create a 3x1 grid of subplots
fig, axs = plt.subplots(3, 1, figsize=(5, 7.5))

# Barplot of AccountSpending
axs[0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['AccountSpending'], color='grey')
axs[0].set(xlabel="Clusters", ylabel="Value", title="Average AccountSpending per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['AccountSpending']):
    axs[0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of CreditCardSpending
axs[1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['CreditCardSpending'], color='grey')
axs[1].set(xlabel="Clusters", ylabel="Value", title="Average CreditCardSpending per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['CreditCardSpending']):
    axs[1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of GrocerySpending
axs[2].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['GrocerySpending'], color='grey')
axs[2].set(xlabel="Clusters", ylabel="Value", title="Average GrocerySpending per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['GrocerySpending']):
    axs[2].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Adjust layout to prevent clipping of titles
plt.tight_layout()

# Show the plots
plt.show()

From the inspection of the two plots, it becomes evident that clusters 3 and 7 are more invested in stocks than their counterparts, and compared to the other clusters also share lower levels of mortgages. Looking at the types of investments, we see that cluster 3 is more invested in NASDAQ listed companies than all other clusters, while cluster 7 is strongly invested in Stratton’s fund for small and mid-size US companies. Spending behavior information tells us that both segments belong to the less spending customers, with cluster 3 showing the lowest credit card turnover of all clusters. In case of grocery expenditures, we see cluster 7 being the cluster with the second-highest average spending behavior.
Last, we can enrich our insights, by looking at the living conditions of the different segments and see where the different segments are located. To achieve this, we finally compare residential information.

In [159]:
# Create a 2x2 grid of subplots
fig, axs = plt.subplots(2, 2, figsize=(17, 12))

# Barplot of MortageVolume
axs[0, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['CityAreaSize'], color='grey')
axs[0, 0].set(xlabel="Clusters", ylabel="Value", title="Average CityAreaSize per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['CityAreaSize']):
    axs[0, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of MeanCityHouseHoldSize
axs[0, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['MeanCityHouseHoldSize'], color='grey')
axs[0, 1].set(xlabel="Clusters", ylabel="Value", title="Average MeanCityHouseHoldSize per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['MeanCityHouseHoldSize']):
    axs[0, 1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of MeanCityIncome
axs[1, 0].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['MeanCityIncome'], color='grey')
axs[1, 0].set(xlabel="Clusters", ylabel="Value", title="Average MeanCityIncome per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['MeanCityIncome']):
    axs[1, 0].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Barplot of MeanCitySqFtPrice
axs[1, 1].bar([cluster + 1 for cluster in summarystats_percluster_7k.index], summarystats_percluster_7k['MeanCitySqFtPrice'], color='grey')
axs[1, 1].set(xlabel="Clusters", ylabel="Value", title="Average MeanCitySqFtPrice per Cluster")

# Add text labels to each bar
for i, value in enumerate(summarystats_percluster_7k['MeanCitySqFtPrice']):
    axs[1, 1].text(i + 1, value, f"{round(value, 2)}", ha='center', va='bottom', color='black', size=8)

# Adjust layout to prevent clipping of titles
plt.tight_layout()

# Show the plots
plt.show()

From the plot we learn that clusters 3 and 7 both prefer city areas with mid-to-lower levels of population. In case of cluster 3 the average household sizes in the residential areas are rather small, while in case of cluster 7 we observe larger compounds with by average 4 members living in one household. Looking at income distributions and the area’s soil values, we learn that cluster 3 lives in rather richer neighborhoods with higher soil prices, whereas cluster 7 members prefer middle-class neighborhoods with affordable, low soil prices.

Question: Combining the information at hand, how do you depict members of clusters 3 and 7 and how do you believe they differ from each other? Can you similarly come up with personae for other clusters?

Taking Actions from Insights

The results of the cluster analysis allow Stratton AE Banking to take several important marketing actions. First, the profound understanding of the different available market segments, allows the joint-venture to understand the different types of customers available and to determine, which segments in the existing customer base should build the base for future marketing activities.

To develop suitable positionings for each cluster and subsequently develop communication campaigns, one can use the further insights from the cluster analysis and the comparison of the cluster-specific means of the remaining variables.

Furthermore, the results of the cluster analysis can be used to also predict the interests and preferences of newly incoming customers. Here, one may use the existing information available and calculate the Euclidean distances between the new customer and the centers (i.e. the means of each dimension) of each cluster. The customer will likely belong to the cluster, with the lowest distance.